/*
 *	Copyright 2011 andro-inject developers 
 *	Licensed under the Apache License, Version 2.0 (the "License"); 
 *	you may not use this file except in compliance with the License. 
 *	You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 * 
 *	Unless required by applicable law or agreed to in writing, software 
 *	distributed under the License is distributed on an "AS IS" BASIS,
 *	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *	See the License for the specific language governing permissions and
 *	limitations under the License. 
 * 
*/

package com.androinject.core.factory;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.androinject.core.ComponentFactory;
import com.androinject.core.definitions.ComponentDefinition;
import com.androinject.core.definitions.ComponentDefinitions;
import com.androinject.core.definitions.ComponentProperty;
import com.androinject.core.definitions.ManagedComponentDefinition;
import com.androinject.core.factory.exceptions.ComponentCreationException;
import com.androinject.core.factory.exceptions.ComponentInstantiationException;
import com.androinject.core.factory.exceptions.IncompatibleComponentDefinitionTypeException;
import com.androinject.core.factory.exceptions.IncompatibleComponentTypeException;
import com.androinject.core.factory.exceptions.IncompatiblePropertyException;
import com.androinject.core.factory.exceptions.NoSuchComponentDefinitionException;
import com.androinject.core.logging.Logger;
import com.androinject.core.logging.Logging;

abstract public class AbstractComponentFactory implements ComponentFactory {
	
	private static Logger log = Logging.getLogger(AbstractComponentFactory.class);
	
	
	protected Map<String, Object> singletons;
	protected Map<String, Object> loadingComponents;
	protected ComponentDefinitions componentDefinitions;
	protected MethodResolver methodResolver;
	protected List<ComponentProcessor> componentProcessors;
	protected Map<String, Class<?>> loadedClasses;
	
	public AbstractComponentFactory() {
		this.methodResolver = new DefaultMethodResolver();
		this.componentProcessors = new LinkedList<ComponentProcessor>();
		this.loadingComponents = new HashMap<String, Object>();
		this.loadedClasses = new HashMap<String, Class<?>>();
	}
	
	
	@Override
	public Object getComponent(String id) {
		if (id == null) {
			throw new IllegalArgumentException("Null is not a valid component id");
		}
		
		Object component = this.singletons.get(id);
		
		if (component != null) {
			return component;
		}
		
		ComponentDefinition definition = this.componentDefinitions.getComponentDefinition(id);
		
		if (definition == null) {
			throw new NoSuchComponentDefinitionException("No Component definition found with id '" + id + "'");
		}
		
		return this.createComponent(definition);
	}
	
	
	@Override
	public <T> T getComponent(String id, Class<T> requiredClass) {
		if (requiredClass == null) {
			throw new IllegalArgumentException("required class cannot be null");
		}
		Object component = this.getComponent(id); // throws NoSuchComponentException if component does not exist
		try {
			return requiredClass.cast(component);
		} catch (Exception e) {
			throw new IncompatibleComponentTypeException("Cannot cast component with id '" + id + "' to class " + requiredClass.getName(), e);
		}
	}
	
	
	@Override
	public void initializeManagedComponent(Object componentToInitialize, String id) {
		if (componentToInitialize == null || id == null) {
			throw new IllegalArgumentException("Passed component and id cannot be null");
		}
		ManagedComponentDefinition definition = this.componentDefinitions.getManagedComponentDefinition(id);
		if (definition instanceof ComponentDefinition) {
			throw new IncompatibleComponentDefinitionTypeException("Cannot initialize managed component with normal component definition.");
		}
		this.processBeforeManagedComponentInjection(componentToInitialize, definition);
		this.handleProperties(componentToInitialize, definition);
		this.processAfterManagedComponentInjection(componentToInitialize, definition);
		this.handleInitMethod(componentToInitialize, definition);
	}
	
	
	@Override
	public void initializeManagedComponent(Object componentToInitialize) {
		if (componentToInitialize == null) {
			throw new IllegalArgumentException("Passed component cannot be null");
		}
		String generatedComponentId = this.generateComponentId(componentToInitialize);
		this.initializeManagedComponent(componentToInitialize, generatedComponentId);
	}
	
	
	public void addComponentProcessor(ComponentProcessor processor) {
		this.componentProcessors.add(processor);
	}
	
	
	@Override
	public Class<?> getType(String id) {
		try {
			ComponentDefinition definition = this.componentDefinitions.getComponentDefinition(id);
			return this.loadClass(definition.getClassName());
		} catch (IncompatibleComponentDefinitionTypeException e) {
			return null;
		} catch (NoSuchComponentDefinitionException e) {
			return null;
		}
	}

	
	public void removeComponentProcessor(ComponentProcessor processor) {
		this.componentProcessors.remove(processor);
	}
	
	
	protected void loadComponents() {
		this.componentDefinitions = this.getComponentDefinitions();
		this.initializeSingletons();
	}
	
	
	protected void initializeSingletons() {
		this.singletons = new HashMap<String, Object>();
		for (ComponentDefinition definition : this.componentDefinitions.listComponentDefinitions()) {
			if (!definition.usesLazyInitialization() && !this.singletons.containsKey(definition.getId())) {
				// singletons.containsKey(..) check is required because its possible that singletons have already been 
				// created in handleProperties() of previous singletons
				this.createComponent(definition);
			}
		}
	}
	
	
	protected Object createComponent(ComponentDefinition componentDefinition) {
		Class<?> clazz = this.loadClass(componentDefinition.getClassName());
		Object instance = this.createInstance(clazz);
		this.loadingComponents.put(componentDefinition.getId(), instance);
		this.processBeforeComponentInjection(instance, componentDefinition);
		this.handleProperties(instance, componentDefinition);
		this.processAfterComponentInjection(instance, componentDefinition);
		this.handleInitMethod(instance, componentDefinition);
		this.loadingComponents.remove(componentDefinition.getId());
		this.singletons.put(componentDefinition.getId(), instance);
		return instance;
	}
	
	
	protected Class<?> loadClass(String className) { // TODO: rename, because of possible caching?
		if (this.loadedClasses.containsKey(className)) {
			return this.loadedClasses.get(className);
		}
		try {
			Class<?> loadedClass = Class.forName(className);
			if (loadedClass == null) {
				throw new NullPointerException();
			}
			this.loadedClasses.put(className, loadedClass);
			return loadedClass;
		} catch (Exception e) {
			throw new ComponentInstantiationException("An error occurred while loading class: " + className, e);
		}
	}
	
	
	protected Object createInstance(Class<?> clazz) {
		try {
			Object instance = clazz.newInstance();
			if (instance == null) {
				throw new NullPointerException();
			}
			return instance;
		} catch (Exception e) {
			throw new ComponentInstantiationException("An error occurred while creating component instance", e);
		}
	}
	
	
	protected void handleProperties(Object component, ManagedComponentDefinition definition) {
		for (ComponentProperty property : definition.getProperties()) {
			String name = property.getName();
			
			Object referencedComponent = null;
			if (this.loadingComponents.containsKey(property.getReferencedComponentId())) {
				referencedComponent = this.loadingComponents.get(property.getReferencedComponentId());
			} else {
				referencedComponent = this.getComponent(property.getReferencedComponentId()); 
			}
			this.inject(component, name, referencedComponent);
		}
	}
	
	
	protected void handleInitMethod(Object component, ManagedComponentDefinition definition) {
		String defaultInitMethodName = this.componentDefinitions.getDefaultInitMethodName();
		String initMethodName = definition.getInitMethod();
		if (initMethodName != null) {
			this.callInitMethod(component, initMethodName);
		} else if (defaultInitMethodName != null) {
			this.callInitMethod(component, defaultInitMethodName);
		}
	}
	
	
	protected void callInitMethod(Object component, String initMethodName) {
		Method[] methods = component.getClass().getMethods();
		
		for (Method method : methods) {
			if (!method.getName().equals(initMethodName)) {
				continue;
			}
			
			try {
				method.invoke(component, new Object[] {});
				return;
			} catch (Exception e) {
				throw new ComponentCreationException("An error occurred while calling init method: " + initMethodName, e);
			}
		}
		throw new ComponentCreationException("Init method not found: " + initMethodName);
	}
	
	
	protected void inject(Object component, String propertyName, Object componentToInject) {
		try {
			Method method = this.methodResolver.getSetter(component, propertyName);
			method.invoke(component, componentToInject);
		} catch (Exception e) {
			throw new IncompatiblePropertyException("An error occurred while injecting property " + propertyName + " into component " + component, e);
		}		
	}

	
	protected String generateComponentId(Object component) {
		Class<?> componentClass = component.getClass();
		String enclosingClass = "";
		if (componentClass.getEnclosingClass() != null) {
			enclosingClass += componentClass.getEnclosingClass().getSimpleName() + "$";
		}
		String classname = enclosingClass + componentClass.getSimpleName();
		classname = classname.substring(0, 1).toLowerCase() + classname.substring(1);
		return classname;
	}
	
	
	protected void processBeforeComponentInjection(Object component, ComponentDefinition definition) {
		for (ComponentProcessor processor : this.componentProcessors) {
			processor.beforeComponentInjection(component, definition);
		}
	}
	
	
	protected void processAfterComponentInjection(Object component, ComponentDefinition definition) {
		for (ComponentProcessor processor : this.componentProcessors) {
			processor.afterComponentInjection(component, definition);
		}
	}
	
	
	protected void processBeforeManagedComponentInjection(Object component, ManagedComponentDefinition definition) {
		for (ComponentProcessor processor : this.componentProcessors) {
			processor.beforeManagedComponentInjection(component, definition);
		}
	}
	
	
	protected void processAfterManagedComponentInjection(Object component, ManagedComponentDefinition definition) {
		for (ComponentProcessor processor : this.componentProcessors) {
			processor.afterManagedComponentInjection(component, definition);
		}
	}
	
	
	abstract protected ComponentDefinitions getComponentDefinitions();
	
}